Micron Document
🎖️GitЯра🎖️


Displaying Raw • Download

core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshService.kt fix/qr-error-correction (7b45f84f) Text, 12.21 KB

T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tf0883e@fileTb4b4b4:Te6edf3SuppressTb4b4b4(Ta5d6ff"Ta5d6ffTooGenericExceptionCaughtTa5d6ff"Tb4b4b4)

Tff7b72package T7ee787org.meshtastic.core.service

Tff7b72import T7ee787android.app.Service
Tff7b72import T7ee787android.content.Context
Tff7b72import T7ee787android.content.Intent
Tff7b72import T7ee787android.os.IBinder
Tff7b72import T7ee787android.os.PowerManager
Tff7b72import T7ee787androidx.core.app.ServiceCompat
Tff7b72import T7ee787co.touchlab.kermit.Logger
Tff7b72import T7ee787kotlinx.coroutines.CoroutineScope
Tff7b72import T7ee787kotlinx.coroutines.Dispatchers
Tff7b72import T7ee787kotlinx.coroutines.Job
Tff7b72import T7ee787kotlinx.coroutines.SupervisorJob
Tff7b72import T7ee787kotlinx.coroutines.cancel
Tff7b72import T7ee787kotlinx.coroutines.flow.first
Tff7b72import T7ee787kotlinx.coroutines.launch
Tff7b72import T7ee787kotlinx.coroutines.withTimeoutOrNull
Tff7b72import T7ee787org.koin.android.ext.android.inject
Tff7b72import T7ee787org.meshtastic.core.common.hasLocationPermission
Tff7b72import T7ee787org.meshtastic.core.common.util.isValidDeviceAddress
Tff7b72import T7ee787org.meshtastic.core.model.DeviceVersion
Tff7b72import T7ee787org.meshtastic.core.model.util.anonymize
Tff7b72import T7ee787org.meshtastic.core.repository.MeshConnectionManager
Tff7b72import T7ee787org.meshtastic.core.repository.MeshNotificationManager
Tff7b72import T7ee787org.meshtastic.core.repository.RadioInterfaceService

T8b949e/**
* Android foreground service that hosts the Meshtastic mesh radio connection.
*
* Acts as the lifecycle anchor for the [MeshServiceOrchestrator], which manages all manager initialization and
* connection state.
*/
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffLargeClassTa5d6ff"Tb4b4b4)
Tff7b72class T56d364MeshService Tb4b4b4: Te6edf3ServiceTb4b4b4(Tb4b4b4) Tb4b4b4{

Tff7b72private Tff7b72val Te6edf3radioInterfaceServiceTb4b4b4: Te6edf3RadioInterfaceService Tff7b72by Te6edf3injectTb4b4b4(Tb4b4b4)

Tff7b72private Tff7b72val Te6edf3connectionManagerTb4b4b4: Te6edf3MeshConnectionManager Tff7b72by Te6edf3injectTb4b4b4(Tb4b4b4)

Tff7b72private Tff7b72val Te6edf3notificationsTb4b4b4: Te6edf3MeshNotificationManager Tff7b72by Te6edf3injectTb4b4b4(Tb4b4b4)

T8b949e/** Android-typed accessor for the foreground service notification. */
Tff7b72private Tff7b72val Te6edf3androidNotificationsTb4b4b4: Te6edf3MeshNotificationManagerImpl
Tff7b72getTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3notifications Tff7b72as Te6edf3MeshNotificationManagerImpl

Tff7b72private Tff7b72val Te6edf3orchestratorTb4b4b4: Te6edf3MeshServiceOrchestrator Tff7b72by Te6edf3injectTb4b4b4(Tb4b4b4)

Tff7b72private Tff7b72val Te6edf3conversationShortcutPublisherTb4b4b4: Te6edf3ConversationShortcutPublisher Tff7b72by Te6edf3injectTb4b4b4(Tb4b4b4)

Tff7b72private Tff7b72var Te6edf3isServiceInitialized Tff7b72= Tff7b72false

T8b949e/**
* Scope for short-lived coroutines owned by this service (e.g. waiting for the selected-device address to load).
* Canceled in [onDestroy].
*/
Tff7b72private Tff7b72val Te6edf3serviceScope Tff7b72= Te6edf3CoroutineScopeTb4b4b4(Te6edf3SupervisorJobTb4b4b4(Tb4b4b4) Tff7b72+ Te6edf3DispatchersTb4b4b4.Te6edf3MainTb4b4b4.Te6edf3immediateTb4b4b4)

T8b949e/**
* Active job waiting for the selected-device address to be loaded from DataStore. Null when no wait is pending
* (either the address was already valid at [onStartCommand] time, or the wait already resolved).
*/
Tff7b72private Tff7b72var Te6edf3addressWaitJobTb4b4b4: Te6edf3Job? Tff7b72= Tff7b72null

T8b949e/**
* Partial wake lock held while the foreground service is running. Prevents the CPU from being throttled while the
* TAK server's keepalive coroutines, socket writes, and mesh packet handlers need to run on a regular cadence.
* Without this, OEM battery optimizations can pause coroutines for long enough that connected TAK clients
* (ATAK/iTAK) time out waiting for data, even though the foreground service itself keeps the process alive.
*/
Tff7b72private Tff7b72var Te6edf3wakeLockTb4b4b4: Te6edf3PowerManagerTb4b4b4.Te6edf3WakeLockTff7b72? Tff7b72= Tff7b72null

Tff7b72companion Tff7b72object Tb4b4b4{
Tff7b72fun Td2a8ffcreateIntentTb4b4b4(Te6edf3contextTb4b4b4: Te6edf3ContextTb4b4b4) Tff7b72= Te6edf3IntentTb4b4b4(Te6edf3contextTb4b4b4, Te6edf3MeshServiceTff7b72::Te6edf3classTb4b4b4.Te6edf3javaTb4b4b4)

Tff7b72val Te6edf3minDeviceVersion Tff7b72= Te6edf3DeviceVersionTb4b4b4(Te6edf3DeviceVersionTb4b4b4.Te6edf3MIN_FW_VERSIONTb4b4b4)
Tff7b72val Te6edf3absoluteMinDeviceVersion Tff7b72= Te6edf3DeviceVersionTb4b4b4(Te6edf3DeviceVersionTb4b4b4.Te6edf3ABS_MIN_FW_VERSIONTb4b4b4)

Tff7b72private Tff7b72const Tff7b72val Te6edf3WAKE_LOCK_TIMEOUT_MS Tff7b72= T79c0ff3T79c0ff0L Tff7b72* T79c0ff6T79c0ff0L Tff7b72* T79c0ff1Te6edf3_000L T8b949e// 30 minutes

T8b949e/**
* How long [onStartCommand] will keep the service alive waiting for the selected-device address flow to emit a
* valid value before concluding that no device is genuinely selected. Covers cold-start DataStore load time;
* only the initial null/blank value is treated as transient.
*/
Tff7b72private Tff7b72const Tff7b72val Te6edf3DEVICE_ADDRESS_SETTLE_MS Tff7b72= T79c0ff5Te6edf3_000L
Tb4b4b4}

Tff7b72override Tff7b72fun Td2a8ffonCreateTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72superTb4b4b4.Te6edf3onCreateTb4b4b4(Tb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffCreating mesh serviceTa5d6ff" Tb4b4b4}

Tff7b72try Tb4b4b4{
Te6edf3orchestratorTb4b4b4.Te6edf3startTb4b4b4(Tb4b4b4)
Te6edf3isServiceInitialized Tff7b72= Tff7b72true
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3IllegalStateExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3eTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffMeshService: DI not ready, stopping serviceTa5d6ff" Tb4b4b4}
Te6edf3stopSelfTb4b4b4(Tb4b4b4)
Tff7b72return
Tb4b4b4}

T8b949e// Keep conversation shortcuts published for the whole service lifetime (not only during an Android Auto
T8b949e// session) so message notifications can link to them and get Conversations-section treatment on phones.
Te6edf3conversationShortcutPublisherTb4b4b4.Te6edf3startObservingTb4b4b4(Te6edf3serviceScopeTb4b4b4)
Tb4b4b4}

Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffReturnCountTa5d6ff"Tb4b4b4)
Tff7b72override Tff7b72fun Td2a8ffonStartCommandTb4b4b4(Te6edf3intentTb4b4b4: Te6edf3Intent?Tb4b4b4, Te6edf3flagsTb4b4b4: Tffa657IntTb4b4b4, Te6edf3startIdTb4b4b4: Tffa657IntTb4b4b4)Tb4b4b4: Tffa657Int Tb4b4b4{
Tff7b72if Tb4b4b4(Tff7b72!Te6edf3isServiceInitializedTb4b4b4) Tb4b4b4{
T8b949e// Also a never-reached-foreground path: stop promptly so the pending-start watchdog cannot fire.
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{ Ta5d6ff"Ta5d6ffonStartCommand called but service is not initialized (likely DI failure). Stopping.Ta5d6ff" Tb4b4b4}
Te6edf3stopServiceCleanlyTb4b4b4(Tb4b4b4)
Tff7b72return Te6edf3START_NOT_STICKY
Tb4b4b4}

T8b949e// Queue the localized status render asynchronously. startForeground uses the last cached notification, or an
T8b949e// immediate application-label fallback, and the renderer updates the same notification ID when it completes.
Te6edf3connectionManagerTb4b4b4.Te6edf3updateStatusNotificationTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3notification Tff7b72= Te6edf3androidNotificationsTb4b4b4.Te6edf3getServiceNotificationTb4b4b4(Tb4b4b4)

T8b949e// Recomputed on every start command, which is load-bearing: MainActivity.onStart() re-issues startService()
T8b949e// on each app open, so a service that began in the background with connectedDevice only (location is
T8b949e// while-in-use restricted there) upgrades to connectedDevice|location the next time the user opens the app.
Tff7b72val Te6edf3foregroundServiceType Tff7b72=
Te6edf3ForegroundStartPolicyTb4b4b4.Te6edf3foregroundServiceTypeTb4b4b4(
Te6edf3hasLocationPermission Tff7b72= Te6edf3hasLocationPermissionTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3appInForeground Tff7b72= Te6edf3isAppInForegroundTb4b4b4(Tb4b4b4)Tb4b4b4,
Tb4b4b4)

T8b949e// Start foreground FIRST. Android requires startForeground() within ~5s of onStartCommand and before any
T8b949e// potentially-blocking work. We never defer this — even when the selected-device address is not yet loaded.
Tff7b72if Tb4b4b4(Tff7b72!Te6edf3startForegroundSafelyTb4b4b4(Te6edf3notificationTb4b4b4, Te6edf3foregroundServiceTypeTb4b4b4)Tb4b4b4) Tb4b4b4{
T8b949e// We were reached via startForegroundService(), so ActivityManager has armed a ~5s watchdog that kills
T8b949e// the process with ForegroundServiceDidNotStartInTimeException unless this service either enters the
T8b949e// foreground or goes away. We could not enter the foreground, so we must go away — immediately, and
T8b949e// without START_STICKY, which would only have the system restart us into the same restricted state.
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{ Ta5d6ff"Ta5d6ffMeshService: could not enter foreground; stopping to release the pending-start watchdogTa5d6ff" Tb4b4b4}
Te6edf3stopServiceCleanlyTb4b4b4(Tb4b4b4)
Tff7b72return Te6edf3START_NOT_STICKY
Tb4b4b4}

Tff7b72val Te6edf3address Tff7b72= Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3getDeviceAddressTb4b4b4(Tb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3isValidDeviceAddressTb4b4b4(Te6edf3addressTb4b4b4)Tb4b4b4) Tb4b4b4{
T8b949e// Address is already loaded and valid — proceed normally.
Te6edf3addressWaitJobTff7b72?.Te6edf3cancelTb4b4b4(Tb4b4b4)
Te6edf3addressWaitJob Tff7b72= Tff7b72null
Te6edf3acquireWakeLockTb4b4b4(Tb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffMeshService: selected device ready (Tffd700${Te6edf3addressTb4b4b4.Te6edf3anonymizeTffd700}Ta5d6ff), staying foregroundTa5d6ff" Tb4b4b4}
Tff7b72return Te6edf3START_STICKY
Tb4b4b4}

T8b949e// Address is currently null/blank/sentinel. This may be a transient state while DataStore emits the persisted
T8b949e// value (cold-start race: RadioPrefsImpl.devAddr starts as null until the first DataStore emission). Make no
T8b949e// irreversible stopSelf() decision here; wait briefly for the address flow to settle.
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffMeshService: selected address not yet loaded (Tffd700${Te6edf3addressTb4b4b4.Te6edf3anonymizeTffd700}Ta5d6ff); waiting for address flowTa5d6ff" Tb4b4b4}
Te6edf3scheduleDeviceAddressResolutionTb4b4b4(Tb4b4b4)
Tff7b72return Te6edf3START_STICKY
Tb4b4b4}

T8b949e/**
* Waits for [RadioInterfaceService.currentDeviceAddressFlow] to emit a valid device address. Resolves the transient
* null/blank window at cold start without busy-polling [RadioInterfaceService.getDeviceAddress].
* - On a valid emission: acquires the wake lock and the service continues as a normal foreground service.
* - On timeout (no valid address observed): concludes no device is genuinely selected and stops cleanly.
*
* Uses [first] with a predicate rather than `drop(1)` so we never skip an already-current valid StateFlow value: if
* the address arrived between the synchronous [onStartCommand] read and this subscription, [first] returns it
* immediately instead of waiting the full timeout and spuriously stopping a service that has a valid device.
*/
Tff7b72private Tff7b72fun Td2a8ffscheduleDeviceAddressResolutionTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3addressWaitJobTff7b72?.Te6edf3cancelTb4b4b4(Tb4b4b4)
Te6edf3addressWaitJob Tff7b72=
Te6edf3serviceScopeTb4b4b4.Te6edf3launch Tb4b4b4{
Tff7b72val Te6edf3resolved Tff7b72=
Te6edf3withTimeoutOrNullTb4b4b4(Te6edf3DEVICE_ADDRESS_SETTLE_MSTb4b4b4) Tb4b4b4{
Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3currentDeviceAddressFlowTb4b4b4.Te6edf3firstTb4b4b4(Tff7b72::Te6edf3isValidDeviceAddressTb4b4b4)
Tb4b4b4}
Tff7b72if Tb4b4b4(Te6edf3isValidDeviceAddressTb4b4b4(Te6edf3resolvedTb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffMeshService: selected device resolved (Tffd700${Te6edf3resolvedTb4b4b4.Te6edf3anonymizeTffd700}Ta5d6ff) after address-flow waitTa5d6ff" Tb4b4b4}
Te6edf3acquireWakeLockTb4b4b4(Tb4b4b4)
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffMeshService: no device selected after address flow settled; stoppingTa5d6ff" Tb4b4b4}
Te6edf3stopServiceCleanlyTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

T8b949e/** Releases everything this service holds and stops it. Safe to call before it ever reached the foreground. */
Tff7b72private Tff7b72fun Td2a8ffstopServiceCleanlyTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3releaseWakeLockTb4b4b4(Tb4b4b4)
Te6edf3ServiceCompatTb4b4b4.Te6edf3stopForegroundTb4b4b4(Tff7b72thisTb4b4b4, Te6edf3ServiceCompatTb4b4b4.Te6edf3STOP_FOREGROUND_REMOVETb4b4b4)
Te6edf3stopSelfTb4b4b4(Tb4b4b4)
Tb4b4b4}

Tff7b72private Tff7b72fun Td2a8ffacquireWakeLockTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3wakeLockTff7b72?.Te6edf3isHeld Tff7b72=Tff7b72= Tff7b72trueTb4b4b4) Tff7b72return
Tff7b72try Tb4b4b4{
Tff7b72val Te6edf3powerManager Tff7b72= Te6edf3getSystemServiceTb4b4b4(Te6edf3POWER_SERVICETb4b4b4) Tff7b72as Te6edf3PowerManager
Tff7b72val Te6edf3lock Tff7b72=
Te6edf3powerManagerTb4b4b4.Te6edf3newWakeLockTb4b4b4(Te6edf3PowerManagerTb4b4b4.Te6edf3PARTIAL_WAKE_LOCKTb4b4b4, Ta5d6ff"Ta5d6ffMeshtastic::MeshServiceWakeLockTa5d6ff"Tb4b4b4)Tb4b4b4.Te6edf3apply Tb4b4b4{
Te6edf3setReferenceCountedTb4b4b4(Tff7b72falseTb4b4b4)
Tb4b4b4}
Te6edf3lockTb4b4b4.Te6edf3acquireTb4b4b4(Te6edf3WAKE_LOCK_TIMEOUT_MSTb4b4b4)
Te6edf3wakeLock Tff7b72= Te6edf3lock
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffAcquired partial wake lock for mesh serviceTa5d6ff" Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3SecurityExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffFailed to acquire wake lock — WAKE_LOCK permission missing?Ta5d6ff" Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffFailed to acquire wake lockTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

Tff7b72private Tff7b72fun Td2a8ffreleaseWakeLockTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3lock Tff7b72= Te6edf3wakeLock Tff7b72?: Tff7b72return
Tff7b72try Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3lockTb4b4b4.Te6edf3isHeldTb4b4b4) Tb4b4b4{
Te6edf3lockTb4b4b4.Te6edf3releaseTb4b4b4(Tb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffReleased partial wake lock for mesh serviceTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffFailed to release wake lockTa5d6ff" Tb4b4b4}
Tb4b4b4} Tff7b72finally Tb4b4b4{
Te6edf3wakeLock Tff7b72= Tff7b72null
Tb4b4b4}
Tb4b4b4}

Tff7b72override Tff7b72fun Td2a8ffonTaskRemovedTb4b4b4(Te6edf3rootIntentTb4b4b4: Te6edf3Intent?Tb4b4b4) Tb4b4b4{
Tff7b72superTb4b4b4.Te6edf3onTaskRemovedTb4b4b4(Te6edf3rootIntentTb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffMesh service: onTaskRemovedTa5d6ff" Tb4b4b4}
Tb4b4b4}

T8b949e// Required by Service — this is a started service (not bound), so always returns null.
Tff7b72override Tff7b72fun Td2a8ffonBindTb4b4b4(Te6edf3intentTb4b4b4: Te6edf3Intent?Tb4b4b4)Tb4b4b4: Te6edf3IBinder? Tff7b72= Tff7b72null

Tff7b72override Tff7b72fun Td2a8ffonDestroyTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffDestroying mesh serviceTa5d6ff" Tb4b4b4}
Te6edf3conversationShortcutPublisherTb4b4b4.Te6edf3stopObservingTb4b4b4(Tb4b4b4)
Te6edf3addressWaitJobTff7b72?.Te6edf3cancelTb4b4b4(Tb4b4b4)
Te6edf3addressWaitJob Tff7b72= Tff7b72null
Te6edf3serviceScopeTb4b4b4.Te6edf3cancelTb4b4b4(Tb4b4b4)
Te6edf3releaseWakeLockTb4b4b4(Tb4b4b4)
Te6edf3ServiceCompatTb4b4b4.Te6edf3stopForegroundTb4b4b4(Tff7b72thisTb4b4b4, Te6edf3ServiceCompatTb4b4b4.Te6edf3STOP_FOREGROUND_REMOVETb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3isServiceInitializedTb4b4b4) Tb4b4b4{
Te6edf3orchestratorTb4b4b4.Te6edf3stopTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tff7b72superTb4b4b4.Te6edf3onDestroyTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Served by rngit 1.5.0 - Generated in 0.06s